Skip to content

4.2 选项

📝 模块更新日志 问题修复*

+ 修复 监听配置文件更改出现多次触发问题 4\.9\.5\.13 ⏱️2024\.09\.29 [feb85d2](https://gitee.com/dotnetchina/Furion/commit/feb85d21ae8e1f1a0dc215dd8b67419c70c8b7ce)
+ 修复 选项 `Options` 不支持启动时进行模型验证问题 4\.9\.5 ⏱️2024\.08\.09 [c54d586](https://gitee.com/dotnetchina/Furion/commit/c54d586858700528a4a39663792ecaf02407f387)

4.2.1 什么是选项

选项是 ASP.NET Core 推荐的动态读取配置的方式,这种方式将配置文件数据用一个强类型来托管,能够实现配置验证、默认值配置、实时读取等功能。

4.2.2 与配置的区别

选项实际上也是配置,但在后者的基础上添加了配置验证、默认值/后期配置设定及提供了多种接口读取配置信息,同时还支持供配置更改通知等强大灵活功能。

所以,除了一次性读取使用的配置以外,都应该选用 选项 替换 配置

知识导航有关配置说明可查看《4.1 配置》 章节。

4.2.3 选项的使用

假设我们需要在系统运行时获取系统名称、版本号及版权信息,这些信息可能随时变化而且需要在多个地方使用。这时就需要将这些信息配置起来。具体步骤如下:

4.2.3.1 配置 appsettings.json 信息

{  
  "AppInfo": {  
    "Name": "Furion",  
    "Version": "1.0.0",  
    "Company": "Baiqian"  
  }  
}  

4.2.3.2 创建 AppInfoOptions 强类型类

using Furion.ConfigurableOptions;  

namespace Furion.Application  
{  
    public class AppInfoOptions : IConfigurableOptions  
    {  
        public string Name { get; set; }  
        public string Version { get; set; }  
        public string Company { get; set; }  
    }  
}  

温馨提示建议所有选项类都应该以 Options 命名结尾。

另外,Furion 框架提供了非常灵活的注册选项服务的方法,只需要继承 IConfigurableOptions 接口即可,该接口位于 Furion.ConfigurableOptions 命名空间下。

4.2.3.3 注册 AppInfoOptions 服务

选项不同于配置,需在应用启动时注册

Furion.Web.Core\FurWebCoreStartup.cs

using Microsoft.AspNetCore.Builder;  
using Microsoft.AspNetCore.Hosting;  
using Microsoft.Extensions.DependencyInjection;  

namespace Furion.Web.Core  
{  
    [AppStartup(800)]  
    public sealed class FurWebCoreStartup : AppStartup  
    {  
        public void ConfigureServices(IServiceCollection services)  
        {  
            services.AddConfigurableOptions<AppInfoOptions>();  
        }  
    }  
}  

4.2.3.4 读取 AppInfoOptions 信息

Furion 框架中,提供了多种读取方式:

  • 通过 App.GetConfig<TOptions>(path) 读取(不推荐
  • 通过依赖注入以下实例读取:
    • IOptions<TOptions>
    • IOptionsSnapshot<TOptions>
    • IOptionsMonitor<TOptions>
  • 通过 App 静态类提供的静态方法获取:
    • App.GetOptions<TOptions>()
    • App.GetOptionsMonitor<TOptions>()
    • App.GetOptionsSnapshot<TOptions>()

特别注意禁止在主机启动时通过 App.GetOptions<TOptions> 获取选项,如需获取配置选项理应通过 App.GetConfig<TOptions>("配置节点", true)

  • App.GetConfig\<TOptions>(path)
  • 依赖注入方式
  • App.GetOptions\<TOptions>()
using Furion.Application;  
using Microsoft.AspNetCore.Mvc;  

namespace Furion.Web.Entry.Controllers  
{  
    [Route("api/[controller]")]  
    public class DefaultController : ControllerBase  
    {  
        [HttpGet]  
        public string Get()  
        {  
            // 不推荐采用此方式读取,该方式仅在 ConfigureServices 启动时使用  
            var appInfo = App.GetConfig<AppInfoOptions>("AppInfo", true);  
            return $@"名称:{appInfo.Name},  
                      版本:{appInfo.Version},  
                      公司:{appInfo.Company}";  
        }  
    }  
}  

using Furion.Application;  
using Microsoft.AspNetCore.Mvc;  
using Microsoft.Extensions.Options;  

namespace Furion.Web.Entry.Controllers  
{  
    [Route("api/[controller]")]  
    public class DefaultController : ControllerBase  
    {  
        private readonly AppInfoOptions options1;  
        private readonly AppInfoOptions options2;  
        private readonly AppInfoOptions options3;  

        public DefaultController(  
            IOptions<AppInfoOptions> options  
            , IOptionsSnapshot<AppInfoOptions> optionsSnapshot  
            , IOptionsMonitor<AppInfoOptions> optionsMonitor)  
        {  
            options1 = options.Value;  
            options2 = optionsSnapshot.Value;  
            options3 = optionsMonitor.CurrentValue;  
        }  

        [HttpGet]  
        public string Get()  
        {  
            var info1 = $@"名称:{options1.Name},  
                      版本:{options1.Version},  
                      公司:{options1.Company}";  

            var info2 = $@"名称:{options2.Name},  
                      版本:{options2.Version},  
                      公司:{options2.Company}";  

            var info3 = $@"名称:{options3.Name},  
                      版本:{options3.Version},  
                      公司:{options3.Company}";  

            return $"{info1}-{info2}-{info3}";  
        }  
    }  
}  

using Furion.Application;  
using Microsoft.AspNetCore.Mvc;  

namespace Furion.Web.Entry.Controllers  
{  
    [Route("api/[controller]")]  
    public class DefaultController : ControllerBase  
    {  
        [HttpGet]  
        public string Get()  
        {  
            var options1 = App.GetOptions<AppInfoOptions>();  
            var info1 = $@"名称:{options1.Name},  
                      版本:{options1.Version},  
                      公司:{options1.Company}";  

            var options2 = App.GetOptionsSnapshot<AppInfoOptions>();  
            var info2 = $@"名称:{options2.Name},  
                      版本:{options2.Version},  
                      公司:{options2.Company}";  

            var options3 = App.GetOptionsMonitor<AppInfoOptions>();  
            var info3 = $@"名称:{options3.Name},  
                      版本:{options3.Version},  
                      公司:{options3.Company}";  

            return $"{info1}-{info2}-{info3}";  
        }  
    }  
}  

4.2.3.5 如何选择读取方式

  • 如果选项需要在多个地方使用,则无论任何时候都不推荐使用 App.GetOptions<TOptions>()
  • 在可依赖注入类中,依赖注入 IOptions[Snapshot|Monitor]<TOptions> 读取
  • 在静态类/非依赖注入类中,选择 App.GetOptions[Snapshot|Monitor]<TOptions>() 读取

4.2.4 选项接口说明

ASP.NET Core 应用提供了多种读取选项的接口:

  • IOptions<TOptions>
    • 不支持:
      • 在应用启动后读取配置数据
      • 命名选项
    • 注册为单一实例且可以注入到任何服务生存期
  • IOptionsSnapshot<TOptions>
    • 在每次请求时应重新计算选项的方案中有用
    • 注册为范围内,因此无法注入到单一实例服务
    • 支持命名选项
  • IOptionsMonitor<TOptions>
    • 用于检索选项并管理 TOptions 实例的选项通知。
    • 注册为单一实例且可以注入到任何服务生存期。
    • 支持:
      • 更改通知
      • 命名选项
      • 可重载配置
      • 选择性选项失效 (IOptionsMonitorCache<TOptions>)

注意事项在使用 IConfigurableOptionsListener 监听选项后,如要获取最新的配置信息,请使用 App.GetOptionsMonitor<TOptions>() 而不是 App.GetOptions<TOptions>()

了解更多想了解更多 选项接口 知识可查阅 ASP.NET Core - 选项 - 选项接口 小节。

4.2.5 选项自定义配置

我们知道,选项实际上需要和配置文件特定键值挂钩,那 Furion 是如何准确的找到配置文件中的键值的呢?

4.2.5.1 选项查找键流程

  • 没有贴 [OptionsSettings] 特性
    • Options 结尾,则去除 Options 字符串
    • 否则返回 类名称
  • 贴了 [OptionsSettings] 特性

    • 如果配置了 Path 属性,则返回 Path 的值
    • 否则返回 类名称
  • 无[OptionsSettings]

  • 有[OptionsSettings]

  • Options 结尾,则键名为:AppInfo

public class AppInfoOptions : IConfigurableOptions  
{  
    public string Name { get; set; }  
    public string Version { get; set; }  
    public string Company { get; set; }  
}  

  • 不以 Options 结尾,则键名为:AppInfoSettings
public class AppInfoSettings : IConfigurableOptions  
{  
    public string Name { get; set; }  
    public string Version { get; set; }  
    public string Company { get; set; }  
}  

  • 配置了 Path 属性,则键名为:AppSettings:AppInfo
[OptionsSettings("AppSettings:AppInfo")]  
public class AppInfoOptions : IConfigurableOptions  
{  
    public string Name { get; set; }  
    public string Version { get; set; }  
    public string Company { get; set; }  
}  

  • 没有配置 Path 属性,,则键名为:AppInfoSettings
[OptionsSettings]  
public class AppInfoSettings : IConfigurableOptions  
{  
    public string Name { get; set; }  
    public string Version { get; set; }  
    public string Company { get; set; }  
}  

4.2.6 [OptionsSettings] 说明

选项类可以通过 [OptionsSettings] 来配置查找路径值。

4.2.7 选项验证

选项支持验证配置有效性,在 Furion 框架中,通过 services.AddConfigurableOptions<TOptions>() 注册选项默认启用了验证支持。

包括:

  • 特性方式 DataAnnotations
  • 自定义复杂验证 IValidateOptions<TOptions>

  • 特性方式

  • 复杂验证
using Furion.ConfigurableOptions;  
using System.ComponentModel.DataAnnotations;  

namespace Furion.Application  
{  
    public class AppInfoOptions : IConfigurableOptions  
    {  
        [Required(ErrorMessage = "名称不能为空")]  
        public string Name { get; set; }  
        [Required, RegularExpression(@"^[0-9][0-9\.]+[0-9]$", ErrorMessage = "不是有效的版本号")]  
        public string Version { get; set; }  
        [Required, MaxLength(100)]  
        public string Company { get; set; }  
    }  
}  

  • 自定义验证类 AppInfoValidation 并继承 IValidateOptions<TOptions> 接口,同时实现 Validate 方法。
using Microsoft.Extensions.Options;  
using System.Text.RegularExpressions;  

namespace Furion.Application  
{  
    public class AppInfoValidation : IValidateOptions<AppInfoOptions>  
    {  
        public ValidateOptionsResult Validate(string name, AppInfoOptions options)  
        {  
            if (!Regex.IsMatch(options.Version, @"^[0-9][0-9\.]+[0-9]$"))  
            {  
                return ValidateOptionsResult.Fail("不是有效的版本号");  
            }  

            return ValidateOptionsResult.Success;  
        }  
    }  
}  

  • 选项类继承 IConfigurableOptions<TOptions, TOptionsValidation> 接口,并实现该接口。
using Furion.ConfigurableOptions;  
using System.ComponentModel.DataAnnotations;  

namespace Furion.Application  
{  
    public class AppInfoOptions : IConfigurableOptions<AppInfoOptions, AppInfoValidation>  
    {  
        [Required(ErrorMessage = "名称不能为空")]  
        public string Name { get; set; }  
        [Required]  
        public string Version { get; set; }  
        [Required, MaxLength(100)]  
        public string Company { get; set; }  

        // 选项后期配置  
        public void PostConfigure(AppInfoOptions options, IConfiguration configuration)  
        {  
        }  
    }  
}  

  • 完整代码如下:
using Furion.ConfigurableOptions;  
using Microsoft.Extensions.Options;  
using System.ComponentModel.DataAnnotations;  
using System.Text.RegularExpressions;  

namespace Furion.Application  
{  
    // 继承 IConfigurableOptions<TOptions, TOptionsValidation> 接口  
    public class AppInfoOptions : IConfigurableOptions<AppInfoOptions, AppInfoValidation>  
    {  
        [Required(ErrorMessage = "名称不能为空")]  
        public string Name { get; set; }  
        [Required]  
        public string Version { get; set; }  
        [Required, MaxLength(100)]  
        public string Company { get; set; }  

        // 选项后期配置  
        public void PostConfigure(AppInfoOptions options)  
        {  
        }  
    }  

    // 创建自定义验证类  
    public class AppInfoValidation : IValidateOptions<AppInfoOptions>  
    {  
        public ValidateOptionsResult Validate(string name, AppInfoOptions options)  
        {  
            if (!Regex.IsMatch(options.Version, @"^[0-9][0-9\.]+[0-9]$"))  
            {  
                return ValidateOptionsResult.Fail("不是有效的版本号");  
            }  

            return ValidateOptionsResult.Success;  
        }  
    }  
}  

特别说明IConfigurableOptions<TOptions, TOptionsValidation> 继承自 IConfigurableOptions<TOptions>,也就是自定义复杂验证默认具有 PostConfigure(TOptions options) 选项后期配置方法。关于《4.2.8 选项后期配置》将在下一小节说明。

4.2.8 选项后期配置

选项后期配置通俗一点来说,可以在运行时解析值或设定默认值/后期配置等。

Furion 框架中,配置选项后期配置很简单,只需要继承 IConfigurableOptions<TOptions> 接口并实现 PostConfigure(TOptions options) 方法。

using Furion.ConfigurableOptions;  
using Microsoft.Extensions.Configuration;  
using System.ComponentModel.DataAnnotations;  

namespace Furion.Application  
{  
    public class AppInfoOptions : IConfigurableOptions<AppInfoOptions>  
    {  
        [Required(ErrorMessage = "名称不能为空")]  
        public string Name { get; set; }  
        [Required]  
        public string Version { get; set; }  
        [Required, MaxLength(100)]  
        public string Company { get; set; }  

        public void PostConfigure(AppInfoOptions options, IConfiguration configuration)  
        {  
            options.Name ??= "Furion";  
            options.Version ??= "1.0.0";  
            options.Company ??= "Baiqian";  
        }  
    }  
}  

特别说明IConfigurableOptions<TOptions, TOptionsValidation> 继承自 IConfigurableOptions<TOptions>,也就是自定义复杂验证默认具有 PostConfigure(TOptions options, IConfiguration configuration) 选项后期配置方法。

4.2.9 选项更改通知(热更新

Furion 框架提供了非常简单且灵活的方式监听选项更改,也就是 appsettings.json 或 自定义配置文件发生任何更改都会触发处理方法

使用非常简单,只需要继承 IConfigurableOptionsListener<TOptions> 接口并实现 void OnListener(TOptions options, IConfiguration configuration) 方法即可。

using Furion.ConfigurableOptions;  

namespace Furion.Application  
{  
    public class AppInfoOptions : IConfigurableOptionsListener<AppInfoOptions>  
    {  
        public string Name { get; set; }  
        public string Version { get; set; }  
        public string Company { get; set; }  

        public void OnListener(AppInfoOptions options, IConfiguration configuration)  
        {  
            var name = options.Name;  // 实时的最新值  
            var version = options.Version;  // 实时的最新值  
        }  

        public void PostConfigure(AppInfoOptions options, IConfiguration configuration)  
        {  
        }  
    }  
}  

特别说明IConfigurableOptionsListener<TOptions> 继承自 IConfigurableOptions<TOptions>

4.2.9.1 关于多次触发问题

Furion 底层使用的是 ChangeToken.OnChange 监听文件更改,但是此方式会导致 OnListener 触发两次,这并非是框架的 bug,而是 .NET Core 本身存在的问题,详见:https://github.com/dotnet/aspnetcore/issues/2542

所以,Furion 框架也给出另一种解决方案可替代 IConfigurableOptionsListener 的方式,也就是通过局部注入 IOptionsMonitor 的方式,如:

public class YourService : IYourService, IDisposable  
{  
    private readonly IDisposable _optionsReloadToken;  

    private YourOptions _options;  

    public YourService(IOptionsMonitor<YourOptions> options)  
    {  
        (_optionsReloadToken, _options) = (options.OnChange(ReloadOptions), options.CurrentValue);  

        // Furion 4.9.5.13+ 版本支持,解决重复触发问题  
        // (_optionsReloadToken, _options) = (options.OnChange(((Action<YourOptions >)ReloadOptions).Debounce()), options.CurrentValue);  // 添加了防抖操作  
    }  

    private void ReloadOptions(YourOptions options)  
    {  
        _options = options;  
    }  

    public void Dispose()  
    {  
        _optionsReloadToken?.Dispose();  
    }  
}  

这种方式虽然啰嗦,但是可以很好和业务代码契合。

解决触发多次问题相关说明:https://gitee.com/dotnetchina/Furion/commit/feb85d21ae8e1f1a0dc215dd8b67419c70c8b7ce#note_32504730

4.2.10 选项的优缺点

  • 优点

    • 强类型配置
    • 提供多种读取方式
    • 支持热加载
    • 支持设置默认值/后期配置
    • 支持在运行环境中动态配置
    • 支持验证配置有效性
    • 支持更改通知
    • 支持命名选项
    • 缺点

    • 需要定义对应类型

    • 需要在启动时注册

4.2.11 自定义属性 Key 映射

版本说明以下内容仅限 Furion v3.4.3+ 版本使用。

有时候我们在 appsettings.json 中配置的 Key 和选项定义的属性名不一样,这时候就需要用到 [MapSettings] 特性即可,如:

"AppInfo": {  
    "Name": "Furion",  
    "Version": "1.0.0",  
    "Company_Name": "Baiqian"  
}  

public class AppInfoOptions : IConfigurableOptions  
{  
    public string Name { get; set; }  
    public string Version { get; set; }  

    [MapSettings("Company_Name")]  
    public string Company { get; set; }  
}  

特别注意[MapSettings] 配置的 Key 会自定应用选项的 Key 作为起始点,如实际上 Company 属性对应的 Key 为:AppInfo:Company_Name

4.2.12 反馈与建议

与我们交流给 Furion 提 Issue


了解更多想了解更多 选项 知识可查阅 ASP.NET Core - 选项 章节。